/*****************************
 *@file: HotUpdate
 *@author: 陈吕唐
 *@desc： 热更新组件 node version_generator.js -v 1.0.0
 *@date: 2022-01-22	15:45
 *****************************/
/****************************
 * 使用
 * 1. 构建项目
 * 2. 修改 version_generator 相关参数 remoteUrl src dest
 * 3. node version_generator.js -v 1.0.0 执行生成 .manifest 文件
 * 4. 将.manifest 和 src 下的 assets src 资源文件放到 远程remoteUrl
 * 5. 修改main.js 在编辑器代码前添加 !function(){if("object"==typeof window.native){var e=localStorage.getItem("HotUpdateSearchPaths");if(e){var i=JSON.parse(e);native.fileUtils.setSearchPaths(i);var s=[],t=i[0]||"",l=t+"_temp/",r=l.length;native.fileUtils.isDirectoryExist(l)&&!native.fileUtils.isFileExist(l+"project.manifest.temp")&&(native.fileUtils.listFilesRecursively(l,s),s.forEach(e=>{var i=e.substr(r),s=t+i;"/"===e[e.length]?native.fileUtils.createDirectory(s):(native.fileUtils.isFileExist(s)&&native.fileUtils.removeFile(s),native.fileUtils.renameFile(e,s))}),native.fileUtils.removeDirectory(l))}}}();
 * 不添加这代码的话,热更新成功后,关闭应用重新打开又是旧的代码
 * ***************************/
/****************************
 * 
 * 
 ****************************/

import { game, native, sys } from 'cc';
import { NATIVE } from 'cc/env';
export default class HotUpdate {
    /****************************************************************************************************************
     *Readonly 常量
     ****************************************************************************************************************/
    /**
     * 热更新资源存储路径
     */
    private readonly STORAGE_PATH = `remotePath/`;

    /**
     * 热更新搜索路径
     */
    private readonly HOT_UP_DATE_SEARCH_PATHS = `HotUpdateSearchPaths`;
    /****************************************************************************************************************
     *Property 定义变量
     ****************************************************************************************************************/
    /**
     * 热更新管理器
     */
    private assetManger: native.AssetsManager = null;


    /**
     * 下载失败重试次数
     */
    protected againDownFailAssetNumber: number = 3;

    /**
     * 热更新配置文件地址
     */
    protected nativeUrl: string = undefined!;

    /**
     * 远程版本
     */
    protected remoteVersion: string = "1.0.0";
    /***************************************************************
     * Call
     ***************************************************************/
    /**
     * 更新完成
     */
    public completeFunc: () => void = undefined!;

    /**
     * 更新失败
     */
    public updateFailFunc: (event: string) => void = undefined!;

    /**
     * 更新中
     */
    public updatingFunc: (event: native.EventAssetsManager) => void = undefined!;

    /**
     * 为大版本更新
     */
    public bigUpdateFunc: () => void = undefined!;
    /****************************************************************************************************************
     * Lifecycle 生命周期相关方法
     ****************************************************************************************************************/
    /**
     * 
     * @param nativeUrl 热更新文件地址
     * @param remoteVersion 远程版本
     */
    constructor(nativeUrl: string, remoteVersion: string) {
        this.nativeUrl = nativeUrl;
        this.remoteVersion = remoteVersion;
        this.initHotUpdate();
    }
    /****************************************************************************************************************
     * Public 公有方法
     ****************************************************************************************************************/
    /****************************************************************************************************************
     * Project 公有方法
     ****************************************************************************************************************/
    /**
     * 判断的热更新是否可用
     * @returns 
     */
    public static isHotUpdate(): boolean {

        if (!sys.isNative) {
            return false;
        }

        if (!NATIVE) {
            return false;
        }

        return true;
    }

    public checkUpdate() {
        if (HotUpdate.isHotUpdate()) {
            this.checkUpdateByRemote();
        } else {
            this.checkUpdateComplete();
        }
    }

    /**
     * 获取本地版本
     * @returns 
     */
    public getLocalVersion(): string {
        if (HotUpdate.isHotUpdate()) {
            return this.manifest.version;
        } else {
            return undefined!;
        }
    }
    /****************************************************************************************************************
     * private 私有方法
     ****************************************************************************************************************/
    /**
     * 检查更新
     */
    private async checkUpdateByRemote() {
        let localVersion = this.manifest.version;
        let remoteVersion = this.remoteVersion;

        if (this.versionBigUpdate(localVersion, remoteVersion)) {
            this.checkBigUpdate();
            return;
        }

        let check = this.versionCompareHandle(localVersion, remoteVersion);
        if (check < 0) {

            this.manifest = this.updateManifestRemoteUrl(this.manifest, remoteVersion);
            await new Promise<void>((resolve, reject) => { setTimeout(() => { resolve() }, 1000) })
            if (this.manifest) {
                this.assetManger.loadLocalManifest(this.manifestFilePath);
                this.assetManger.setEventCallback(this.checkUpdateEvent.bind(this));
                this.assetManger.checkUpdate();
            } else {
                this.checkUpdateFail(`写入Manifest失败`);
            }
        } else {
            this.checkUpdateComplete();
        }
    }


    /**
     * 监听版本检查相关事件
     * @param event 
     */
    private checkUpdateEvent(event: native.EventAssetsManager) {
        switch (event.getEventCode()) {
            case native.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
            case native.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
            case native.EventAssetsManager.ERROR_PARSE_MANIFEST:
                this.checkUpdateFail(event.getMessage());
                break;
            case native.EventAssetsManager.NEW_VERSION_FOUND:
                this.checkUpNew();
                break;
            case native.EventAssetsManager.ALREADY_UP_TO_DATE:
                //已更新到最新版本
                this.checkUpdateComplete();
                break;
            default:
                break;
        }
    }

    /**
     * 监听热更新事件
     * @param event 
     */
    private hotUpdateEvent(event: native.EventAssetsManager) {
        switch (event.getEventCode()) {
            case native.EventAssetsManager.UPDATE_PROGRESSION:
                //更新中 更新进度
                this.updating(event)
                break;
            case native.EventAssetsManager.UPDATE_FAILED:
                this.updateFailAgain(event);
                break;
            case native.EventAssetsManager.UPDATE_FINISHED:
                //更新完成
                this.updateGameComplete();
                break;
            default:
                break;
        }
    }
    /****************************************************
     * Init
     ***************************************************/
    /**
     * 在热更新位置创建和初始一样的备份
     * @param path 文件位置
     * @param defaultAssets 用于备份的资源
     */
    private initManifestFilePath(path: string = this.manifestFilePath, nativeUrl: string = this.nativeUrl) {
        if (!native.fileUtils.isDirectoryExist(this.storagePath)) {
            native.fileUtils.createDirectory(this.storagePath);
        }
        /**
         * 创建热更新位置的Project
         */
        let read = native.fileUtils.getStringFromFile(nativeUrl);
        return native.fileUtils.writeStringToFile(read, path);
    }

    /**
     * 初始热更新管理器
     * @param assetsNativeUrl 
     * @returns 
     */
    private initHotUpdate(): void {
        if (!HotUpdate.isHotUpdate()) {
            return;
        }

        /**
         * 热更新文件存放地址(一定要能写入地址,不然下载资源失败)
         */

        this.assetManger = new native.AssetsManager(``, this.storagePath, this.versionCompareHandle);
        this.assetManger.setVerifyCallback(this.assetsVerify.bind(this))
    }
    /****************************************************
     * 热更新管理器事件处理
     ***************************************************/
    /**
     * 检测到新版本处理
     */
    private checkUpNew() {
        //发现有新版本
        this.assetManger.setEventCallback(this.hotUpdateEvent.bind(this));
        this.assetManger.update();
    }

    /**
     * 更新失败处理(下载失败重试)
     */
    private updateFailAgain(event: native.EventAssetsManager) {
        if (this.againDownFailAssetNumber > 0) {
            this.assetManger.downloadFailedAssets();
            this.againDownFailAssetNumber--;
        } else {
            this.checkUpdateFail(event.getMessage());
        }
    }


    /**
     * 热更新游戏完成(重启游戏)
     */
    private updateGameComplete() {
        let paths: string[] = this.assetManger.getLocalManifest().getSearchPaths();

        if (paths && paths.length > 0) {
            localStorage.setItem(this.HOT_UP_DATE_SEARCH_PATHS, JSON.stringify(paths));
        }

        setTimeout(() => {
            game.restart();
        }, 3000)
    }
    /****************************************
     * 更新结果处理 
     ****************************************/
    /**
     * 更新中
     */
    private updating(event: native.EventAssetsManager) {
        if (this.updatingFunc) {
            this.updatingFunc(event);
        }
    }
    /**
     * 更新失败
     * @param event 
     */
    private checkUpdateFail(event: string) {
        if (this.updateFailFunc) {
            this.updateFailFunc(event)
        }
    }


    /**
     * 检查更新结果为大版本更新需要重新下载游戏
     */
    private checkBigUpdate() {
        if (this.bigUpdateFunc) {
            this.bigUpdateFunc();
        }
    }

    /**
     * 检查更新完成,进入游戏
     */
    private checkUpdateComplete() {
        if (this.completeFunc) {
            this.completeFunc();
        }
    }
    /****************************************
     * Utils
     ***************************************/
    /**
     * 是否为大版本更新需要重新下载安装包
     * @param versionA 当前版本
     * @param versionB 远程版本
     * @returns true 是大版本需要重新下载 false 只需要热更新就好了
     */
    protected versionBigUpdate(versionA: string, versionB: string): boolean {
        let vA = versionA.split('.');
        let vB = versionB.split('.');

        let a = parseInt(vA[0]);
        let b = parseInt(vB[0]);

        return a < b;
    }

    /**
     * 资源验证
     * @param path 
     * @param assets 
     */
    protected assetsVerify(path: string, assets: native.ManifestAsset): boolean {
        return true;
    }

    /**
     * 
     * @param versionA 本地版本
     * @param versionB 远程版本
     * @returns 
     */
    private versionCompareHandle(versionA: string, versionB: string): number {
        // //`当前版本 :  ${versionA} , 远程版本 : ${versionB}`);
        let vA = versionA.split('.');
        let vB = versionB.split('.');
        for (let i = 0; i < vA.length && i < vB.length; ++i) {
            let a = parseInt(vA[i]);
            let b = parseInt(vB[i]);
            if (a === b) {
                continue;
            }
            else {
                return a - b;
            }
        }
        if (vB.length > vA.length) {
            return -1;
        }
        return 0;
    }


    /**
     * 修改manifest远程资源路径(本方法未写入文件)
     * @param manifest 资源
     * @param version 版本
     * @returns 
     */
    private updateManifestRemoteUrl(manifest: Manifest, version: string): Manifest {
        let beforePackageUrl: string = manifest.packageUrl;

        /**
         * 远程地址字符串去除版本
         */
        let latestDir = "remote-assets";
        let position = beforePackageUrl.lastIndexOf(latestDir);
        if (position > 0) {
            beforePackageUrl = beforePackageUrl.substring(0, position + latestDir.length);
        }
        if (beforePackageUrl[beforePackageUrl.length - 1] == "/") {
            beforePackageUrl = beforePackageUrl.substring(0, beforePackageUrl.length - 1);
        }

        /**
         * 加上新的版本字符串
         */
        let afterPackageUrl = `${beforePackageUrl}${version}/`;

        /**
         * 写入文件
         */
        manifest.packageUrl = afterPackageUrl;
        manifest.remoteManifestUrl = afterPackageUrl + "project.manifest";
        manifest.remoteVersionUrl = afterPackageUrl + "version.manifest";

        return manifest;
    }
    /****************************************************************************************************************
     * Property Get && Set 属性的 get && set 函数
     ****************************************************************************************************************/
    /**
     * 热更新资源存放位置
     */
    private get storagePath(): string {
        /**
         * 热更新文件存放地址(一定要能写入地址,不然下载资源失败)
         */
        return native.fileUtils.getWritablePath() + `/${this.STORAGE_PATH}`;;
    }

    /**
     * 实际读取的热更新配置文件地址
     */
    private get manifestFilePath(): string {
        return `${this.storagePath}project.manifest`;
    }

    /**
     * 热更新文件
     */
    private get manifest(): Manifest {
        let manifest: Manifest = undefined!;

        /**
         * 文件位置
         */
        let localFilePath = this.manifestFilePath

        if (!native.fileUtils.isFileExist(localFilePath)) {
            this.initManifestFilePath();
        }

        manifest = JSON.parse(native.fileUtils.getStringFromFile(localFilePath));
        return manifest;
    }

    private set manifest(value: Manifest) {
        /**
         * 文件位置
         */
        let localFilePath = this.manifestFilePath;

        /**
         * 写入的Json字符串
         */
        let writeString = JSON.stringify(value);

        native.fileUtils.writeStringToFile(writeString, localFilePath)
    }
}

/**
 * 热更新配置文件结构
 */
interface Manifest {
    packageUrl: string;
    remoteManifestUrl: string;
    remoteVersionUrl: string;
    version: string;
}
